package hu.vissy.yacap.parser;

import hu.vissy.notification.Notification;
import hu.vissy.notification.Notification.Level;
import hu.vissy.yacap.OptionSet;
import hu.vissy.yacap.exception.ParseInterruptedException;
import hu.vissy.yacap.option.CommandLineOption;
import hu.vissy.yacap.option.ValuePolicy;

import java.util.Arrays;

public class CommandLineParser {

    private OptionSet optionSet;

    private ParserOptions options;

    /**
     * Constructor with default options.
     *
     * @param optionSet
     *            The associated option set.
     */
    public CommandLineParser(OptionSet optionSet) {
        this(optionSet, new ParserOptions());
    }

    /**
     * @param optionSet
     *            The associated option set.
     * @param options
     *            The parser options.
     *
     */
    public CommandLineParser(OptionSet optionSet, ParserOptions options) {
        setOptionSet(optionSet);
        this.options = options;
    }

    /**
     * @return the optionSet
     */
    public OptionSet getOptionSet() {
        return optionSet;
    }

    /**
     * @param optionSet
     *            the optionSet to set
     */
    public final void setOptionSet(OptionSet optionSet) {
        this.optionSet = optionSet;
    }

    public ParserResult parse(String[] args) {
        ParserResult res = new ParserResult(optionSet);

        try {

            int p = 0;
            while (p < args.length) {
                // Reading the next argument
                String val = args[p];
                if (val.trim().length() == 0) {
                    p++;
                    continue;
                }

                // We reached unprocessable arguments
                if (val.equals("--")) {
                    res.setInternalParameters(Arrays.copyOfRange(args, p + 1, args.length));
                    break;
                }

                // If it doesn't start with hyphen, we reached the unnamed
// arguments
                if (!val.startsWith("-")) {
                    int sp = p++;
                    while (p < args.length && !args[p].trim().equals("--")) {
                        if (options.isCheckMisplacedUnvaluedOption() && args[p].startsWith("-")) {
                            String r = "";
                            if (sp > 0) {
                                r = " Possibly the unvalued option " + args[sp - 1] + " got a value.";
                            }
                            addMessage(res, new Notification(Level.WARNING, "Misplaced option: " + args[p] + "." + r));
                        }
                        p++;
                    }
                    res.setUnnamedParameters(Arrays.copyOfRange(args, sp, p));
                    // Step back to let the parser process the last argument.
                    continue;
                }

                Object opVal = null;

                if (!val.startsWith("--") && val.length() > 2) {
                    // Block of short form specified, value-less parameters
                    for (int i = 1; i < val.length(); i++) {
                        // Process all options
                        CommandLineOption<?> op = optionSet.findByShortForm(val.charAt(i));
                        if (op == null) {
                            addMessage(res, new Notification(Level.ERROR, "Unknown option: -" + val.charAt(i)));
                        }
                        if (op.getValuePolicy() == ValuePolicy.REQUIRED) {
                            addMessage(res, new Notification(Level.ERROR, "Combined options should be non-value types. Option: -" + val.charAt(i)));
                        } else if (op.getValuePolicy() == ValuePolicy.NONE) {
                            opVal = true;
                        } else {
                            opVal = op.getDefaultValue();
                        }
                        res.setValue(op, opVal);
                    }
                } else {
                    // Value with long form or non-combined short form
                    CommandLineOption<?> op = null;
                    if (val.startsWith("--")) {
                        // Long form
                        op = optionSet.findByLongForm(val.substring(2));
                    } else {
                        // Short form
                        op = optionSet.findByShortForm(val.charAt(1));
                    }

                    if (op == null) {
                        addMessage(res, new Notification(Level.ERROR, "Unknown option: " + val));
                    }

                    switch (op.getValuePolicy()) {
                    case NONE:
                        opVal = op.getDefaultValue();
                        break;
                    case OPTIONAL:
                        // Peek ahead
                        String nextString = null;
                        if (p + 1 < args.length) {
                            nextString = args[p + 1];
                        }
                        // Next argument is not an option, so we take it as a
// value
                        // for this option.
                        if (!nextString.startsWith("-")) {
                            opVal = op.parseValue(nextString);
                            p++;
                        } else {
                            opVal = op.getDefaultValue();
                        }
                        break;
                    case REQUIRED:
                        if (p == args.length - 1) {
                            addMessage(res, new Notification(Level.ERROR, "Option should have a value specified: " + val));
                        }
                        String opStr = args[++p];
                        if (opStr.startsWith("-")) {
                            addMessage(res, new Notification(Level.ERROR, "Option should have a value specified: " + val));
                        }
                        try {
                            opVal = op.parseValue(opStr);
                        } catch (Exception e) {
                            addMessage(res, new Notification(Level.ERROR, "Option tranformation error: " + e.getMessage() + " (Option: " + op.getLongForm()
                                            + ", value: " + opStr + ")"));
                        }
                        break;
                    }

                    res.setValue(op, opVal);
                }
                p++;
            }
        } catch (ParseInterruptedException e) {
            // Do nothing
        }

        return res;
    }

    private void addMessage(ParserResult res, Notification notification) throws ParseInterruptedException {
        res.addMessage(notification);
        if (options.getStopOnMessageLevel().getWeight() <= notification.getLevel().getWeight()) {
            throw new ParseInterruptedException();
        }
    }
}
